import gc
import random

import DMuse.Artear.utils.util
import remote
import dawdreamer as dawdmr
import musicpy as mp
import numpy as np


class NoteSet:
    def __init__(self):
        self.degree = []
        self.duration = []
        self.interval = []
        self.start_time = 0

    def is_empty(self):
        return not self.degree or not self.duration or not self.interval

    def to_musicpy_chord(self):
        return mp.chord([mp.degree_to_note(i) for i in self.degree], duration=self.duration, interval=self.interval,
                        start_time=self.start_time)


def musicpy_chord_to_noteset(chord):
    ns = NoteSet()
    for i, interval in zip(range(len(chord)), chord.interval):
        ns.degree.append(chord[i].degree)
        ns.duration.append(chord[i].duration)
        ns.interval.append(interval)
        ns.start_time = chord.start_time
    return ns


class FX:
    def __init__(self, name, plugin_path=''):
        self.name = name
        self.plugin_path = plugin_path
        self.params = []

    def apply_to_backbone(self, backbone):
        if isinstance(backbone, dawdmr.PluginProcessor):
            for index, param in enumerate(self.params):
                backbone.set_parameter(index, param)


class Track:
    def __init__(self):
        self.fx_list = []
        self.notes = NoteSet()


class DAW:
    def __init__(self, backend='reaper',
                 bpm=120,
                 track_length=4,
                 sample_rate=44100,
                 buffer_size=256,
                 cwd='.',
                 apply_immediately=False,
                 **config):
        self.bpm = bpm
        self.track_length = track_length
        self.sample_rate = sample_rate
        self.buffer_size = buffer_size
        self.cwd = cwd
        self.apply_immediately = apply_immediately
        self.config = config

        self.backend_name = backend
        if backend == 'reaper':
            self.backend = remote.ReaperRemote()
        elif backend == 'dawdreamer':
            self.backend = dawdmr.RenderEngine(sample_rate=sample_rate, block_size=buffer_size)
        else:
            self.backend = None

        self.tracks = []
        self.dawdmr_graph = []

    def add_track(self):
        self.tracks.append(Track())
        if self.apply_immediately:
            if isinstance(self.backend, remote.ReaperRemote):
                self.backend.wake()
                self.backend.insert_track(-1)
            elif isinstance(self.backend, dawdmr.RenderEngine):
                self.dawdmr_graph.append((None, []))
                self.backend.load_graph(self.dawdmr_graph)

    def add_fx(self, track_id, fx_name, plugin_path=''):
        fx = FX(fx_name, plugin_path)
        if self.apply_immediately:
            if isinstance(self.backend, remote.ReaperRemote):
                self.backend.wake()
                self.backend.add_trackfx(track_id, fx_name)
            elif isinstance(self.backend, dawdmr.RenderEngine):
                instr = self.backend.make_plugin_processor('{}-{}'.format(track_id, fx_name), plugin_path)
                self.dawdmr_graph[track_id] += [(instr, [])]
                self.backend.load_graph(self.dawdmr_graph)
        self.tracks[track_id].fx_list.append(fx)

    def set_fx_params(self, params, track_id=-1, fx_id=-1):
        self.tracks[track_id].fx_list[fx_id].params = params
        if self.apply_immediately:
            if isinstance(self.backend, remote.ReaperRemote):
                self.backend.wake()
                self.backend.set_params(track_id=track_id, fx_id=fx_id, params=params)
            elif isinstance(self.backend, dawdmr.RenderEngine):
                fx = self.dawdmr_graph[track_id][fx_id]
                self.tracks[track_id].fx_list[fx_id].apply_to_backbone(fx)
                self.backend.load_graph(self.dawdmr_graph)

    def set_notes(self, noteset: NoteSet, track_id=-1, apply_immediately=False):
        if self.apply_immediately or apply_immediately:
            if isinstance(self.backend, remote.ReaperRemote):
                self.backend.wake()
                self.backend.delete_first_item()
                self.backend.create_midi_item(0, self.track_length, track_id=track_id, no_new=True)
                self.backend.set_notes(noteset.to_musicpy_chord(), track_id=-1)
            elif isinstance(self.backend, dawdmr.RenderEngine):
                instr = self.dawdmr_graph[track_id][0]
                mp.write(self.tracks[track_id].notes.to_musicpy_chord(), bpm=self.bpm, name=self.cwd+'/temp.mid')
                instr.load_midi(self.cwd + '/temp.mid')
                self.backend.load_graph(self.dawdmr_graph)
        self.tracks[track_id].notes = noteset

    def delete_all_tracks(self, apply_immediately=False):
        if self.apply_immediately or apply_immediately:
            if isinstance(self.backend, remote.ReaperRemote):
                self.backend.wake()
                self.backend.delete_tracks(len(self.tracks))
            elif isinstance(self.backend, dawdmr.RenderEngine):
                self.backend.load_graph([])
        self.tracks.clear()
        gc.collect()

    def add_tracks(self, n_tracks, apply_immediately=False):
        if self.apply_immediately or apply_immediately:
            if isinstance(self.backend, remote.ReaperRemote):
                self.backend.wake()
                self.backend.add_tracks(n_tracks)
            elif isinstance(self.backend, dawdmr.RenderEngine):
                self.backend.load_graph([(None, []) for _ in range(n_tracks)])
        self.tracks = [Track() for _ in range(n_tracks)]

    def load_midi(self, midi_path, track_id=-1):
        noteset = musicpy_chord_to_noteset(mp.read(midi_path).tracks[0])
        self.set_notes(noteset, track_id=track_id)

    def load_preset(self, preset_path, track_id=-1, fx_id=-1):
        simple_daw = DMuse.Artear.utils.simpledaw.SimpleDAW(self.tracks[track_id].fx_list[fx_id].plugin_path)
        params = simple_daw.get_params(preset_path)
        self.tracks[track_id].fx_list[fx_id].params = params

    def set_bpm(self, bpm):
        if self.apply_immediately:
            if isinstance(self.backend, remote.ReaperRemote):
                self.backend.wake()
                self.backend.set_bpm(bpm)
            elif isinstance(self.backend, dawdmr.RenderEngine):
                self.backend.set_bpm(bpm)

    def to_numpy(self):
        # data = np.zeros((64, 10, 128))
        length = int(self.track_length * 16)
        midi_data = np.zeros((len(self.tracks), length, 128))
        for i in range(len(self.tracks)):
            ns = self.tracks[i].notes
            t = ns.start_time
            for deg, dura, inte in zip(ns.degree, ns.duration, ns.interval):
                if t * 16 >= length:
                    break
                print(deg, dura, int(t * 16))
                midi_data[i, int(t * 16), deg] = dura
                t += inte

        if self.tracks and self.tracks[0].fx_list:
            fx_data = np.zeros((len(self.tracks), len(self.tracks[0].fx_list[0].params)))
            for i in range(len(self.tracks)):
                for j, param in enumerate(self.tracks[0].fx_list[0].params):
                    fx_data[i, j] = param
        else:
            fx_data = np.zeros((len(self.tracks), 1))
        return midi_data, fx_data

    def from_numpy(self, midi_array, fx_array):
        for i in range(midi_array.shape[0]):
            ns = NoteSet()
            lst = []

            for j in range(midi_array.shape[1]):
                for k in range(midi_array.shape[2]):
                    if midi_array[i, j, k] >= 1/16:
                        lst.append((j, k, midi_array[i, j, k]))

            lst.sort()
            t = 0.
            ns.start_time = t
            for j, k, val in lst:
                ns.degree.append(k)
                ns.duration.append(val)
                ns.interval.append(j / 16 - t)
                print(k, val, j / 16 - t)
                t = j / 16

            self.tracks[i].notes = ns

        for i in range(fx_array.shape[0]):
            self.tracks[i].fx_list[0].params = list(fx_array[i, :])

    def apply_to_daw(self):
        n_tracks = len(self.tracks)
        if isinstance(self.backend, remote.ReaperRemote):
            self.backend.wake()
            self.backend.delete_tracks(len(self.tracks))
            self.backend.wake()
            self.backend.add_tracks(n_tracks)
        elif isinstance(self.backend, dawdmr.RenderEngine):
            self.backend.load_graph([(None, []) for _ in range(n_tracks)])
        for i in range(len(self.tracks)):
            self.set_notes(self.tracks[i].notes, track_id=i, apply_immediately=True)

    def render(self, seconds=5):
        if self.apply_immediately:
            if isinstance(self.backend, remote.ReaperRemote):
                self.backend.wake()
                self.backend.render()
            elif isinstance(self.backend, dawdmr.RenderEngine):
                self.backend.render(seconds)

    def close(self):
        if isinstance(self.backend, remote.ReaperRemote):
            self.backend.close()


if __name__ == '__main__':
    import DMuse.Artear.utils.simpledaw
    import DMuse.Artear.utils.util
    import time

    daw = DAW(backend='reaper', track_length=4, apply_immediately=False)

    daw.add_track()
    daw.add_fx(-1, 'Sylenth1 (LennarDigital)', plugin_path='C:/VST/64bit/Sylenth1.dll')

    simple_daw = DMuse.Artear.utils.simpledaw.SimpleDAW('C:/VST/64bit/Sylenth1.dll')
    fxp_files = DMuse.Artear.utils.util.get_files(r"B:\muse_repo\Presets", 'fxp')
    params = simple_daw.get_params(random.choice(fxp_files))

    daw.set_fx_params(params, -1, -1)

    mid_files = DMuse.Artear.utils.util.get_files(r"B:\muse_repo\MIDI\chord", 'mid')

    daw.load_midi(random.choice(mid_files), track_id=-1)

    midi_data, fx_data = daw.to_numpy()
    print(midi_data.shape, fx_data.shape)
    # daw.apply_to_daw()
    time.sleep(4)
    daw.from_numpy(midi_data, fx_data)
    daw.apply_to_daw()

    # daw.render()

    daw.close()
